/**
 * Copyright (c) 2011-2019, James Zhan 詹波 (jfinal@126.com).
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.jfinal.validate;

import com.jfinal.aop.Interceptor;
import com.jfinal.aop.Invocation;
import com.jfinal.core.Controller;
import com.jfinal.kit.LogKit;
import com.jfinal.kit.StrKit;

import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Validator.
 */
public abstract class Validator implements Interceptor {

    protected Controller controller;
    protected Invocation invocation;
    protected boolean shortCircuit = false;
    protected boolean invalid = false;
    protected String datePattern = null;

    /**
     * TODO set the DEFAULT_DATE_PATTERN in Const and config it in Constants. TypeConverter do the same thing.
     */
    protected static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
    protected static final String emailAddressPattern = "\\b(^['_A-Za-z0-9-]+(\\.['_A-Za-z0-9-]+)*@([A-Za-z0-9-])+(\\.[A-Za-z0-9-]+)*((\\.[A-Za-z0-9]{2,})|(\\.[A-Za-z0-9]{2,}\\.[A-Za-z0-9]{2,}))$)\\b";

    /**
     * 参数校验错误前缀
     */
    public static final String ERR_ = "err_";

    /**
     * 设置短路验证. 默认值为 false
     * 短路验证是指在验证过程中，只要碰到验证失败则立即停止后续验证并返回
     * 非短路验证是指验证操作一直持续到结束，无论中途有没有碰到验证失败
     *
     * @param shortCircuit true 表示短路型验证
     */
    protected void setShortCircuit(boolean shortCircuit) {
        this.shortCircuit = shortCircuit;
    }

    protected void setDatePattern(String datePattern) {
        this.datePattern = datePattern;
    }

    protected String getDatePattern() {
        return (datePattern != null ? datePattern : DEFAULT_DATE_PATTERN);
    }

    @Override
    final public void intercept(Invocation invocation) {
        Validator validator = null;
        try {
            validator = getClass().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        validator.controller = invocation.getController();
        validator.invocation = invocation;

        try {
            validator.validate(validator.controller);
        } catch (ValidateException e) {
            // should not be throw, short circuit validate need this
            LogKit.logNothing(e);
        }

        if (validator.invalid) {
            validator.handleError(validator.controller);
        } else {
            invocation.invoke();
        }
    }

    /**
     * Use validateXxx method to validate the parameters of this action.
     */
    protected abstract void validate(Controller c);

    /**
     * Handle the validate error.
     * Example:<br>
     * controller.keepPara();<br>
     * controller.render("register.html");
     */
    protected abstract void handleError(Controller c);

    /**
     * Add message when validate failure.
     */
    protected void addError(String errorKey, String errorMessage) {
        invalid = true;
        controller.addError(errorKey, errorMessage);
        if (shortCircuit) {
            throw new ValidateException();
        }
    }

    protected void addFieldError(String field, String errorMessage) {
        this.addError(ERR_ + field, errorMessage);
    }

    /**
     * Return the controller of this action.
     */
    protected Controller getController() {
        return controller;
    }

    /**
     * Return the action key of this action.
     */
    protected String getActionKey() {
        return invocation.getActionKey();
    }

    /**
     * Return the controller key of this action.
     */
    protected String getControllerKey() {
        return invocation.getControllerKey();
    }

    /**
     * Return the method of this action.
     */
    protected Method getActionMethod() {
        return invocation.getMethod();
    }

    /**
     * Return the method name of this action.
     */
    protected String getActionMethodName() {
        return invocation.getMethodName();
    }

    /**
     * Return view path of this controller.
     */
    protected String getViewPath() {
        return invocation.getViewPath();
    }

    /**
     * Validate Required. Allow space characters.
     */
    protected void validateRequired(String field, String errorMessage) {
        String value = controller.getPara(field);
        if (value == null || "".equals(value)) {    // 经测试,form表单域无输入时值为"",跳格键值为"\t",输入空格则为空格" "
            addError(ERR_ + field, errorMessage);
        }
    }

    /**
     * Validate Required for urlPara.
     */
    protected void validateRequired(int index, String errorKey, String errorMessage) {
        String value = controller.getPara(index);
        if (value == null /* || "".equals(value) */) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate required string.
     */
    protected void validateRequiredString(String field, String errorMessage) {
        if (StrKit.isBlank(controller.getPara(field))) {
            addError(ERR_ + field, errorMessage);
        }
    }

    /**
     * Validate required string for urlPara.
     */
    protected void validateRequiredString(int index, String errorKey, String errorMessage) {
        if (StrKit.isBlank(controller.getPara(index))) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate integer.
     */
    protected void validateInteger(String field, int min, int max, String errorMessage) {
        validateIntegerValue(controller.getPara(field), min, max, ERR_ + field, errorMessage);
    }

    /**
     * Validate integer for urlPara.
     */
    protected void validateInteger(int index, int min, int max, String errorKey, String errorMessage) {
        String value = controller.getPara(index);
        if (value != null && (value.startsWith("N") || value.startsWith("n"))) {
            value = "-" + value.substring(1);
        }
        validateIntegerValue(value, min, max, errorKey, errorMessage);
    }

    private void validateIntegerValue(String value, int min, int max, String errorKey, String errorMessage) {
        if (StrKit.isBlank(value)) {
            addError(errorKey, errorMessage);
            return;
        }
        try {
            int temp = Integer.parseInt(value.trim());
            if (temp < min || temp > max) {
                addError(errorKey, errorMessage);
            }
        } catch (Exception e) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate integer.
     */
    protected void validateInteger(String field, String errorMessage) {
        validateIntegerValue(controller.getPara(field), ERR_ + field, errorMessage);
    }

    /**
     * Validate integer for urlPara.
     */
    protected void validateInteger(int index, String errorKey, String errorMessage) {
        String value = controller.getPara(index);
        if (value != null && (value.startsWith("N") || value.startsWith("n"))) {
            value = "-" + value.substring(1);
        }
        validateIntegerValue(value, errorKey, errorMessage);
    }

    private void validateIntegerValue(String value, String errorKey, String errorMessage) {
        if (StrKit.isBlank(value)) {
            addError(errorKey, errorMessage);
            return;
        }
        try {
            Integer.parseInt(value.trim());
        } catch (Exception e) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate long.
     */
    protected void validateLong(String field, long min, long max, String errorMessage) {
        validateLongValue(controller.getPara(field), min, max, ERR_ + field, errorMessage);
    }

    /**
     * Validate long for urlPara.
     */
    protected void validateLong(int index, long min, long max, String errorKey, String errorMessage) {
        String value = controller.getPara(index);
        if (value != null && (value.startsWith("N") || value.startsWith("n"))) {
            value = "-" + value.substring(1);
        }
        validateLongValue(value, min, max, errorKey, errorMessage);
    }

    private void validateLongValue(String value, long min, long max, String errorKey, String errorMessage) {
        if (StrKit.isBlank(value)) {
            addError(errorKey, errorMessage);
            return;
        }
        try {
            long temp = Long.parseLong(value.trim());
            if (temp < min || temp > max) {
                addError(errorKey, errorMessage);
            }
        } catch (Exception e) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate long.
     */
    protected void validateLong(String field, String errorMessage) {
        validateLongValue(controller.getPara(field), ERR_ + field, errorMessage);
    }

    /**
     * Validate long for urlPara.
     */
    protected void validateLong(int index, String errorKey, String errorMessage) {
        String value = controller.getPara(index);
        if (value != null && (value.startsWith("N") || value.startsWith("n"))) {
            value = "-" + value.substring(1);
        }
        validateLongValue(value, errorKey, errorMessage);
    }

    private void validateLongValue(String value, String errorKey, String errorMessage) {
        if (StrKit.isBlank(value)) {
            addError(errorKey, errorMessage);
            return;
        }
        try {
            Long.parseLong(value.trim());
        } catch (Exception e) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate double.
     */
    protected void validateDouble(String field, double min, double max, String errorMessage) {
        String errorKey = ERR_ + field;
        String value = controller.getPara(field);
        if (StrKit.isBlank(value)) {
            addError(errorKey, errorMessage);
            return;
        }
        try {
            double temp = Double.parseDouble(value.trim());
            if (temp < min || temp > max) {
                addError(errorKey, errorMessage);
            }
        } catch (Exception e) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate double.
     */
    protected void validateDouble(String field, String errorMessage) {
        String errorKey = ERR_ + field;
        String value = controller.getPara(field);
        if (StrKit.isBlank(value)) {
            addError(errorKey, errorMessage);
            return;
        }
        try {
            Double.parseDouble(value.trim());
        } catch (Exception e) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate date. Date formate: yyyy-MM-dd
     */
    protected void validateDate(String field, String errorMessage) {
        String value = controller.getPara(field);
        String errorKey = ERR_ + field;
        if (StrKit.isBlank(value)) {
            addError(errorKey, errorMessage);
            return;
        }
        try {
            new SimpleDateFormat(getDatePattern()).parse(value.trim());    // Date temp = Date.valueOf(value); 为了兼容 64位 JDK
        } catch (Exception e) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate date.
     */
    protected void validateDate(String field, Date min, Date max, String errorMessage) {
        String errorKey = ERR_ + field;
        String value = controller.getPara(field);
        if (StrKit.isBlank(value)) {
            addError(errorKey, errorMessage);
            return;
        }
        try {
            Date temp = new SimpleDateFormat(getDatePattern()).parse(value.trim());    // Date temp = Date.valueOf(value); 为了兼容 64位 JDK
            if (temp.before(min) || temp.after(max)) {
                addError(errorKey, errorMessage);
            }
        } catch (Exception e) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate date. Date formate: yyyy-MM-dd
     */
    protected void validateDate(String field, String min, String max, String errorMessage) {
        // validateDate(field, Date.valueOf(min), Date.valueOf(max), errorKey, errorMessage);  为了兼容 64位 JDK
        String errorKey = ERR_ + field;
        try {
            SimpleDateFormat sdf = new SimpleDateFormat(getDatePattern());
            validateDate(field, sdf.parse(min.trim()), sdf.parse(max.trim()), errorMessage);
        } catch (Exception e) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate equal field. Usually validate password and password again
     */
    protected void validateEqualField(String field_1, String field_2, String errorKey, String errorMessage) {
        String value_1 = controller.getPara(field_1);
        String value_2 = controller.getPara(field_2);
        if (value_1 == null || value_2 == null || (!value_1.equals(value_2))) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate equal string.
     */
    protected void validateEqualString(String s1, String s2, String errorKey, String errorMessage) {
        if (s1 == null || s2 == null || (!s1.equals(s2))) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate equal integer.
     */
    protected void validateEqualInteger(Integer i1, Integer i2, String errorKey, String errorMessage) {
        if (i1 == null || i2 == null || (i1.intValue() != i2.intValue())) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate email.
     */
    protected void validateEmail(String field, String errorMessage) {
        validateRegex(field, emailAddressPattern, false, errorMessage);
    }

    /**
     * Validate URL.
     */
    protected void validateUrl(String field, String errorMessage) {
        String errorKey = ERR_ + field;
        String value = controller.getPara(field);
        if (StrKit.isBlank(value)) {
            addError(errorKey, errorMessage);
            return;
        }
        try {
            value = value.trim();
            if (value.startsWith("https://")) {
                value = "http://" + value.substring(8); // URL doesn't understand the https protocol, hack it
            }
            new URL(value);
        } catch (MalformedURLException e) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate regular expression.
     */
    protected void validateRegex(String field, String regExpression, boolean isCaseSensitive, String errorMessage) {
        String errorKey = ERR_ + field;
        String value = controller.getPara(field);
        if (value == null) {
            addError(errorKey, errorMessage);
            return;
        }
        Pattern pattern = isCaseSensitive ? Pattern.compile(regExpression) : Pattern.compile(regExpression, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(value);
        if (!matcher.matches()) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate regular expression and case sensitive.
     */
    protected void validateRegex(String field, String regExpression, String errorMessage) {
        validateRegex(field, regExpression, true, errorMessage);
    }

    /**
     * Validate string.
     */
    protected void validateString(String field, int minLen, int maxLen, String errorMessage) {
        validateStringValue(controller.getPara(field), minLen, maxLen, ERR_ + field, errorMessage);
    }

    /**
     * Validate string for urlPara
     */
    protected void validateString(int index, int minLen, int maxLen, String errorKey, String errorMessage) {
        validateStringValue(controller.getPara(index), minLen, maxLen, errorKey, errorMessage);
    }

    private void validateStringValue(String value, int minLen, int maxLen, String errorKey, String errorMessage) {
        if (StrKit.isBlank(value)) {
            addError(errorKey, errorMessage);
            return;
        }
        if (value.length() < minLen || value.length() > maxLen) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate token created by Controller.createToken(String).
     */
    protected void validateToken(String tokenName, String errorKey, String errorMessage) {
        if (controller.validateToken(tokenName) == false) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * Validate token created by Controller.createToken().
     */
    protected void validateToken(String errorKey, String errorMessage) {
        if (controller.validateToken() == false) {
            addError(errorKey, errorMessage);
        }
    }

    /**
     * validate boolean.
     */
    protected void validateBoolean(String field, String errorMessage) {
        validateBooleanValue(controller.getPara(field), ERR_ + field, errorMessage);
    }

    /**
     * validate boolean for urlPara.
     */
    protected void validateBoolean(int index, String errorKey, String errorMessage) {
        validateBooleanValue(controller.getPara(index), errorKey, errorMessage);
    }

    private void validateBooleanValue(String value, String errorKey, String errorMessage) {
        if (StrKit.isBlank(value)) {
            addError(errorKey, errorMessage);
            return;
        }
        value = value.trim().toLowerCase();
        if ("1".equals(value) || "true".equals(value)) {
            return;
        } else if ("0".equals(value) || "false".equals(value)) {
            return;
        }
        addError(errorKey, errorMessage);
    }

    protected void validateCaptcha(String field, String errorMessage) {
        if (getController().validateCaptcha(field) == false) {
            addError(ERR_ + field, errorMessage);
        }
    }
}



